前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket

# 使用 eventproxy 控制并发

30 秒速记

  • eventproxy = 事件聚合器:等几个异步结果都到齐了,再统一执行回调,解决回调嵌套
  • 两个主力:ep.all('a', 'b', fn) 等几个不同事件;ep.after('x', n, fn) 等同一事件 n 次
  • 它只负责「等齐」,不负责限流,请求早就一次全发出去了
  • 失败要接住:ep.fail(cb) 或每个请求的错误也 emit,不然少一次事件就永远等不齐
  • 现在的等价写法:Promise.all / Promise.allSettled + async/await

eventproxy 的作用是把几个并行的异步结果汇到一起,等它们全部完成再统一处理,省得把请求一层层嵌套在回调里。 用法是先说好等哪些事件,ep.all 或 ep.after,然后每个请求完成时 emit 一下,都到齐就触发回调。这其实就是 Promise.all 出现之前的写法,思路完全一样。很多人误会它能控制并发,其实不行,它只是等,请求在发出那一刻就全出去了;要限流还得配 async 的 mapLimit 这类调度工具。

理解「使用 eventproxy 控制并发」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 event-loop 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

eventproxy 是早期 Node 社区为了解决「回调地狱」做的事件聚合库。核心只有两个动作:注册「等什么」,以及在异步操作完成时 emit。

const EventProxy = require('eventproxy');
const ep = new EventProxy();

// 等三个不同的数据源都到齐
ep.all('user', 'posts', 'config', (user, posts, config) => {
  console.log(user.name, posts.length, config.theme);
});

ep.fail((err) => console.error('有一个失败了', err)); // 任一 ep.done 收到错误时触发

getUser(ep.done('user'));      // ep.done 自动处理 (err, data) 回调:成功就 emit,失败就交给 fail
getPosts(ep.done('posts'));
getConfig(ep.done('config'));
@前端进阶之旅: 代码已经复制到剪贴板

换成今天的写法:

try {
  const [user, posts, config] = await Promise.all([getUser(), getPosts(), getConfig()]);
  console.log(user.name, posts.length, config.theme);
} catch (err) {
  console.error('有一个失败了', err);
}
@前端进阶之旅: 代码已经复制到剪贴板

两者表达的东西一样,区别在错误处理:Promise.all 天然带错误传播,而 eventproxy 如果用的是裸 emit 又没在失败分支里处理,失败的那个事件永远不会来,回调就永远不执行,程序也不报错,这是用它时最常见的坑。所以读老代码时,看到 ep.emit 先确认每条失败路径都有着落。

面试官追问

追问 1用了 eventproxy,为什么还是一下子发出去几百个请求?
参考回答

eventproxy 只管等事件到齐,什么时候发请求是你的循环决定的。forEach 里直接发,就是全部同时发。要限流得在发起那一侧加任务池。

追问 2脚本偶尔永远不退出,也不输出 JSON,最可能是什么原因?
参考回答

有个请求失败了没 emit,ep.after 等的次数永远差一次。失败也要 emit(带上错误标记),或者用 ep.fail 统一接错误,再给每个请求加超时。

追问 3ep.all 和 Promise.all 有什么对应关系?
参考回答

ep.all('a', 'b', fn) 约等于 Promise.all([pa, pb]).then(([a, b]) => fn(a, b))。区别是 Promise.all 有一个失败会直接 reject,而 eventproxy 不显式处理错误就只会卡住。

追问 4老项目里一堆 eventproxy,要不要全改成 Promise?
参考回答

改动多、稳定的老代码不急着动。要改就先给现有行为写测试,覆盖成功、部分失败、超时,再一处处替换,保证错误处理和输出不变。

追问 5请求都结束了,进程却迟迟不退出,跟 eventproxy 有关吗?
参考回答

一般不是它,是还有资源挂着,比如 keep-alive 连接、定时器。不过监听器挂在长期存在的对象上不解绑,也会一直持有回调里的上下文,用完记得 ep.unbind()。

# 《使用 eventproxy 控制并发》

# 目标

30 秒速记

  • 任务:node app.js 输出 CNode 首页所有主题的 title、href、comment1,JSON 格式
  • 两层请求:先抓首页拿到主题链接,再并发抓每个详情页取第一条评论
  • 工具组合:superagent 发请求、cheerio 解析 HTML、eventproxy 等所有详情页到齐
  • 结果要靠 href 对应,别依赖完成顺序;没评论给 null,抓取失败要单独标记
  • 结果走 stdout,调试日志走 stderr,不然下游解析 JSON 会失败

这一节的需求是写一个小爬虫:抓 CNode 首页所有主题,再进每个主题页拿第一条评论,最后输出成 JSON 数组。 难点在第二步,几十个详情页请求是并行发出去的,什么时候全部回来不确定,所以要用 eventproxy 的 ep.after 等够次数再统一处理。每次 emit 时把 href 和页面内容一起带上,因为回来的顺序是乱的,不带 href 就对不上号。最后组装时没有评论的主题也要保留,comment1 给 null。

建立一个 lesson4 项目,在其中编写代码。

代码的入口是 app.js,当调用 node app.js 时,它会输出 CNode( https://cnodejs.org/ ) 社区首页的所有主题的标题,链接和第一条评论,以 json 的格式。

输出示例:

[
  {
    "title": "【公告】发招聘帖的同学留意一下这里",
    "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12",
    "comment1": "呵呵呵呵"
  },
  {
    "title": "发布一款 Sublime Text 下的 JavaScript 语法高亮插件",
    "href": "http://cnodejs.org/topic/54207e2efffeb6de3d61f68f",
    "comment1": "沙发!"
  }
]
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1输出里 title 和 comment1 偶尔对不上号,为什么?
参考回答

按完成顺序往数组里塞结果,而请求回来的顺序是随机的。emit 时要带上 [href, html],组装时按 href 对应;要保持首页顺序,就按首页链接数组的下标回填。

追问 2抓取、解析、输出全写在一个回调里,怎么拆?
参考回答

拆成三个函数:fetchTopicUrls() 返回链接数组,fetchFirstComment(url) 返回单条结果,main() 负责调度和输出。每个函数能单独测,页面结构变了也只改解析那一块。

追问 3任意一个详情页失败,是整体失败还是输出部分结果?
参考回答

看下游要什么。要求必须完整就整体失败,退出码非 0;允许降级就输出成功项,失败项带上 error 字段,同时把失败数打到 stderr。最怕的是静默少几条。

追问 4把调试日志 console.log 了一堆,下游 JSON.parse 失败了,怎么改?
参考回答

stdout 只放最终的 JSON,调试信息改用 console.error 打到 stderr。这样 node app.js > out.json 拿到的就是干净的数据。

# 挑战

30 秒速记

  • 在上一题基础上,每条再加 author1(首评作者)和 score1(作者积分)
  • 多了一层依赖:详情页 → 解析出作者 → 再请求作者主页拿积分,第三层请求要等第二层结果
  • 同一个作者可能评论了多个主题,按作者缓存请求,避免重复抓
  • 两层请求都要算进并发预算,只限第一层,第二层可能瞬间爆量
  • 缺失要区分:没评论、作者页打不开、积分确实是 0,别统一填 0

这个挑战把问题从「并行等齐」变成了「有依赖的两级请求」:拿到详情页才知道作者是谁,拿到作者才能去查积分。 写法上,每个主题内部是串行的,详情页完成后再请求作者页;不同主题之间还是并行。有两个容易忽略的点:一是首页上很多评论来自同一批活跃用户,按用户名缓存请求能省掉一大半;二是第二层请求也得限流,不然第一层限住了,第二层一口气冒出几十个请求,照样被封。

以上文目标为基础,输出 comment1 的作者,以及他在 cnode 社区的积分值。

示例:

[
  {
    "title": "【公告】发招聘帖的同学留意一下这里",
    
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket
完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

使用 eventproxy 控制并发|Nodejs专题系列

# 使用 eventproxy 控制并发

30 秒速记

  • eventproxy = 事件聚合器:等几个异步结果都到齐了,再统一执行回调,解决回调嵌套
  • 两个主力:ep.all('a', 'b', fn) 等几个不同事件;ep.after('x', n, fn) 等同一事件 n 次
  • 它只负责「等齐」,不负责限流,请求早就一次全发出去了
  • 失败要接住:ep.fail(cb) 或每个请求的错误也 emit,不然少一次事件就永远等不齐
  • 现在的等价写法:Promise.all / Promise.allSettled + async/await

eventproxy 的作用是把几个并行的异步结果汇到一起,等它们全部完成再统一处理,省得把请求一层层嵌套在回调里。 用法是先说好等哪些事件,ep.all 或 ep.after,然后每个请求完成时 emit 一下,都到齐就触发回调。这其实就是 Promise.all 出现之前的写法,思路完全一样。很多人误会它能控制并发,其实不行,它只是等,请求在发出那一刻就全出去了;要限流还得配 async 的 mapLimit 这类调度工具。

理解「使用 eventproxy 控制并发」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 event-loop 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

eventproxy 是早期 Node 社区为了解决「回调地狱」做的事件聚合库。核心只有两个动作:注册「等什么」,以及在异步操作完成时 emit。

const EventProxy = require('eventproxy');
const ep = new EventProxy();

// 等三个不同的数据源都到齐
ep.all('user', 'posts', 'config', (user, posts, config) => {
  console.log(user.name, posts.length, config.theme);
});

ep.fail((err) => console.error('有一个失败了', err)); // 任一 ep.done 收到错误时触发

getUser(ep.done('user'));      // ep.done 自动处理 (err, data) 回调:成功就 emit,失败就交给 fail
getPosts(ep.done('posts'));
getConfig(ep.done('config'));
@前端进阶之旅: 代码已经复制到剪贴板

换成今天的写法:

try {
  const [user, posts, config] = await Promise.all([getUser(), getPosts(), getConfig()]);
  console.log(user.name, posts.length, config.theme);
} catch (err) {
  console.error('有一个失败了', err);
}
@前端进阶之旅: 代码已经复制到剪贴板

两者表达的东西一样,区别在错误处理:Promise.all 天然带错误传播,而 eventproxy 如果用的是裸 emit 又没在失败分支里处理,失败的那个事件永远不会来,回调就永远不执行,程序也不报错,这是用它时最常见的坑。所以读老代码时,看到 ep.emit 先确认每条失败路径都有着落。

面试官追问

追问 1用了 eventproxy,为什么还是一下子发出去几百个请求?
参考回答

eventproxy 只管等事件到齐,什么时候发请求是你的循环决定的。forEach 里直接发,就是全部同时发。要限流得在发起那一侧加任务池。

追问 2脚本偶尔永远不退出,也不输出 JSON,最可能是什么原因?
参考回答

有个请求失败了没 emit,ep.after 等的次数永远差一次。失败也要 emit(带上错误标记),或者用 ep.fail 统一接错误,再给每个请求加超时。

追问 3ep.all 和 Promise.all 有什么对应关系?
参考回答

ep.all('a', 'b', fn) 约等于 Promise.all([pa, pb]).then(([a, b]) => fn(a, b))。区别是 Promise.all 有一个失败会直接 reject,而 eventproxy 不显式处理错误就只会卡住。

追问 4老项目里一堆 eventproxy,要不要全改成 Promise?
参考回答

改动多、稳定的老代码不急着动。要改就先给现有行为写测试,覆盖成功、部分失败、超时,再一处处替换,保证错误处理和输出不变。

追问 5请求都结束了,进程却迟迟不退出,跟 eventproxy 有关吗?
参考回答

一般不是它,是还有资源挂着,比如 keep-alive 连接、定时器。不过监听器挂在长期存在的对象上不解绑,也会一直持有回调里的上下文,用完记得 ep.unbind()。

# 《使用 eventproxy 控制并发》

# 目标

30 秒速记

  • 任务:node app.js 输出 CNode 首页所有主题的 title、href、comment1,JSON 格式
  • 两层请求:先抓首页拿到主题链接,再并发抓每个详情页取第一条评论
  • 工具组合:superagent 发请求、cheerio 解析 HTML、eventproxy 等所有详情页到齐
  • 结果要靠 href 对应,别依赖完成顺序;没评论给 null,抓取失败要单独标记
  • 结果走 stdout,调试日志走 stderr,不然下游解析 JSON 会失败

这一节的需求是写一个小爬虫:抓 CNode 首页所有主题,再进每个主题页拿第一条评论,最后输出成 JSON 数组。 难点在第二步,几十个详情页请求是并行发出去的,什么时候全部回来不确定,所以要用 eventproxy 的 ep.after 等够次数再统一处理。每次 emit 时把 href 和页面内容一起带上,因为回来的顺序是乱的,不带 href 就对不上号。最后组装时没有评论的主题也要保留,comment1 给 null。

建立一个 lesson4 项目,在其中编写代码。

代码的入口是 app.js,当调用 node app.js 时,它会输出 CNode( https://cnodejs.org/ ) 社区首页的所有主题的标题,链接和第一条评论,以 json 的格式。

输出示例:

[
  {
    "title": "【公告】发招聘帖的同学留意一下这里",
    "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12",
    "comment1": "呵呵呵呵"
  },
  {
    "title": "发布一款 Sublime Text 下的 JavaScript 语法高亮插件",
    "href": "http://cnodejs.org/topic/54207e2efffeb6de3d61f68f",
    "comment1": "沙发!"
  }
]
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1输出里 title 和 comment1 偶尔对不上号,为什么?
参考回答

按完成顺序往数组里塞结果,而请求回来的顺序是随机的。emit 时要带上 [href, html],组装时按 href 对应;要保持首页顺序,就按首页链接数组的下标回填。

追问 2抓取、解析、输出全写在一个回调里,怎么拆?
参考回答

拆成三个函数:fetchTopicUrls() 返回链接数组,fetchFirstComment(url) 返回单条结果,main() 负责调度和输出。每个函数能单独测,页面结构变了也只改解析那一块。

追问 3任意一个详情页失败,是整体失败还是输出部分结果?
参考回答

看下游要什么。要求必须完整就整体失败,退出码非 0;允许降级就输出成功项,失败项带上 error 字段,同时把失败数打到 stderr。最怕的是静默少几条。

追问 4把调试日志 console.log 了一堆,下游 JSON.parse 失败了,怎么改?
参考回答

stdout 只放最终的 JSON,调试信息改用 console.error 打到 stderr。这样 node app.js > out.json 拿到的就是干净的数据。

# 挑战

30 秒速记

  • 在上一题基础上,每条再加 author1(首评作者)和 score1(作者积分)
  • 多了一层依赖:详情页 → 解析出作者 → 再请求作者主页拿积分,第三层请求要等第二层结果
  • 同一个作者可能评论了多个主题,按作者缓存请求,避免重复抓
  • 两层请求都要算进并发预算,只限第一层,第二层可能瞬间爆量
  • 缺失要区分:没评论、作者页打不开、积分确实是 0,别统一填 0

这个挑战把问题从「并行等齐」变成了「有依赖的两级请求」:拿到详情页才知道作者是谁,拿到作者才能去查积分。 写法上,每个主题内部是串行的,详情页完成后再请求作者页;不同主题之间还是并行。有两个容易忽略的点:一是首页上很多评论来自同一批活跃用户,按用户名缓存请求能省掉一大半;二是第二层请求也得限流,不然第一层限住了,第二层一口气冒出几十个请求,照样被封。

以上文目标为基础,输出 comment1 的作者,以及他在 cnode 社区的积分值。

示例:

[
  {
    "title": "【公告】发招聘帖的同学留意一下这里",
    "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12",
    "comment1": "呵呵呵呵",
    "author1": "auser",
    "score1": 80
  },
  ...
]
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1首评作者的主页打不开,score1 填 0 可以吗?
参考回答

不行,0 是一个真实的积分值,填了就是造数据。给 null,再加个 error 字段说明原因,让下游能区分「积分为 0」和「没拿到」。

追问 240 个主题里一半的首评是同一个人,怎么避免重复请求他的主页?
参考回答

按用户名缓存 Promise 本身,而不是缓存结果:cache[name] ??= fetchScore(name)。这样即使几个请求同时到,也只会发一次。失败时把这条缓存删掉,别把错误永久缓存下来。

追问 3只给详情页请求加了并发上限,作者页没加,会出什么问题?
参考回答

详情页一批批返回后,作者页请求会集中涌出来,峰值可能远超上限。最简单的是两层共用一个全局任务池,所有对外请求都经过它。

追问 4score1 偶尔挂到了别的作者名下,怎么复现?
参考回答

给作者页请求加随机延迟打乱返回顺序,就能稳定复现。根因一般是按完成顺序或共享变量回填,改成用用户名做键、每个主题的结果对象自己持有作者和积分。

# 知识点

← 使用 async 控制并发使用 superagent 与 cheerio 完成简单爬虫 →

fe